#!/bin/bash

# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

IFCONFIG=/bin/ifconfig
LSUSB=/usr/sbin/lsusb
LSPCI=/usr/sbin/lspci
IW=/usr/sbin/iw
ARP=/sbin/arp
ARPING=/sbin/arping
TLSDATE=/usr/bin/tlsdate
ETHTOOL=/usr/sbin/ethtool
anonymize_macs=${ANONYMIZE_MACS-yes}
fail_count=0
declare -A fn_entry

# Annotate function entry.
fn_enter () {
  local fn="$1"
  fn_entry["${fn}"]=1
  echo "Entering $*"
}

# Annotate function entry.  Also, return non-zero if function has
# been called more than once
fn_enter_once () {
  local fn="$1"
  [ -n "${fn_entry["${fn}"]}" ] && return 1
  fn_enter "$@"
}

# Output pass/fail reports
pass () {
    echo "PASS: $*"
}
fail () {
    echo "FAIL: $*"
    fail_count="$((fail_count+1))"
}

# Create a unique filename -- this is a "non secure" implementation (we would
# have used mktemp if that was the concern), but instead we want to create an
# easy way to tell which file is more recent.  Also, make a symlink to the most
# recent file
unique_file () {
    local out_template="$1"
    out_file="$(echo "${out_template}" | \
      sed -e 's/\*/'$(date +'%Y-%m-%d.%H:%M:%S')'/')"
    link_file="$(echo "${out_template}" | sed -e 's/\*/latest/')"
    rm -f "${out_file}" "${link_file}"
    ln -s "$(basename $out_file)" "${link_file}"
    echo "$out_file"
}

# Split a dotted decimal string
do_address_parts () {
  (IFS=" ."; echo $1)
}

# Perform a mask (binary "&") between two dotted-decimal strings
do_netmask () {
  local -a ip=($(do_address_parts "$1"))
  local -a mask=($(do_address_parts "$2"))
  local -a ret
  for part in ${!ip[@]}; do
    ret+=("$((ip[part] & mask[part]))")
  done
  (IFS=.; echo "${ret[*]}")
}

# Search an entry from the routing table, given the destination IP address
# @ip: Destination IP address
# @route_flags: Search for this value in the "Flags" column
# @part: Column from the netstat output to return
get_route () {
  local ip="$1"
  local route_flags="$2"
  shift ; shift
  netstat -nr | while read line; do
    local -a netstat=(${line})
    if [ "${netstat[3]}" = "${route_flags}" -a \
      "$(do_netmask "${ip}" "${netstat[2]}")" = "${netstat[0]}" ] ; then
      for part in "$@"; do
        echo "${netstat["${part}"]}"
      done
      return 0
    fi
  done
  return 1
}

# Return the interface through which traffic to given neigbor IP should go
get_if_route () {
  get_route "$1" U 7
}

# Return the gateway IP to which traffic to given remote IP should be forwarded
get_gw_route () {
  get_route "$1" UG 1 7
}

# Trace down a symbolic link until we reach a dead-end (or a real file)
get_tracelink () {
  local file="$1"
  local link_count=0
  while [ -h "${file}" -o -e "${file}" ] ; do
    ls -ld "${file}"
    new_file="$(readlink -f "${file}")"
    [ "${new_file}" = "${file}" ] && return
    if [ "${link_count}" -ge 10 ] ; then
      fail "Gave up after ${link_count} steps"
      return
    fi
    link_count="$((link_count + 1))"
    file="${new_file}"
  done
  fail "${file} does not exist"
}

# Return the IP addresses of the currently configured nameservers
get_nameservers () {
  awk '/^nameserver/ {print $2}' /etc/resolv.conf
}

# Return list of interface names
get_iflist () {
  ls /sys/class/net/ | egrep -v '^(lo|sit)'
}

# Return the interface name that has the default route
get_default_if () {
  route -n | awk '/^0.0.0.0/ { print $2; exit 0 }'
}

# Return IP address of a given network interface
get_if_addr () {
  ifc="$1"
  ${IFCONFIG} "${ifc}" | grep "inet addr" | sed  -e 's/.*addr://; s/ .*//'
}

# Super-gross method for looking up IP address of a given hostname
get_host_addr () {
  # TODO(pstew): super gross -- but there's no host/dig/nslookup/dnsquery
  local host="$1"
  ping -c1 "${host}" | head -1 | awk -v FS='[()]' '{print $2}'
}

# Find a the device name of a device given the driver's name
get_class_driver () {
  local find_pat="$1"
  local find_driver="$2"
  for device in /sys/class/${find_pat}*; do
    local driver="$(basename $(readlink "${device}/device/driver"))"
    if [ "${driver}" = "${find_driver}" ] ; then
      echo "$(basename "${device}")"
      return 0
    fi
  done
}

# The "status" command can't be run as non-root, so we fake it here
get_status () {
  pid="$(pgrep $1)"
  if [ -n "${pid}" ] ; then
    pass "$1 is running, pid ${pid}"
    return 0
  else
    fail "$1 is stopped"
    return 1
  fi
}

# Join the rest of the arguments using the first argument as a delimiter.
join () {
  local delim="$1"
  shift
  echo $(IFS="${delim}"; echo "$*")
}

# Compose a string from hex characters.
hex_to_string () {
  local hex
  for hex in "$@"; do
    printf "\x${hex}"
  done
  printf "\n"
}

seconds_compare () {
  local test_time="$1"
  local reference_time="$2"
  if [ $test_time -gt $reference_time ] ; then
    echo "$((test_time - reference_time)) seconds from now"
  else
    echo "$((reference_time - test_time)) seconds ago"
  fi
}

# Read a DHCPCD lease file
read_dhcp_lease () {
  local file="$1"
  local message_types=(UNKNOWN DISCOVER OFFER REQUEST DECLINE \
                       ACK NACK RELEASE INFORM)
  local now="$(date +%s)"
  local lease_start="$(stat -c %Y $file)"
  local lease_age="$((now-lease_start))"
  echo "  Lease file age: $lease_age"
  local lease_int=($(od --address-radix=n --format=u1 \
                         --output-duplicates ${file}))
  local lease_hex=($(od --address-radix=n --format=x1 \
                         --output-duplicates ${file}))
  echo "  Client MAC address: $(join : ${lease_hex[*]:28:6})"
  echo "  Leased address: $(join . ${lease_int[*]:16:4})"
  local server_address="$(join . ${lease_int[*]:20:4})"
  if [ "${server_address}" != "0.0.0.0" ]; then
    echo "  Server address: ${server_address}"
  fi
  if [ "${lease_hex[*]:236:4}" != "63 82 53 63" ] ; then
    echo "  This lease file does not contain the DHCP magic number (RFC 2131)"
    return
  fi
  local idx=240
  while [ -n "${lease_int[$idx]}" -a -n "${lease_int[$((idx+1))]}" ] ; do
    local opt="${lease_int[$idx]}"
    local len="${lease_int[$((idx+1))]}"
    local data_int=(${lease_int[*]:$((idx+2)):$len})
    local data_hex=(${lease_hex[*]:$((idx+2)):$len})
    idx="$((idx + 2 + len))"
    case $opt in
      0)
        break
        ;;
      1)
        echo "  Netmask: $(join . ${data_int[*]})"
        ;;
      6)
        local -a dns_servers
        local server_index=0
        while [ -n "${data_hex[$server_index]}" ] ; do
          dns_server="$(join . "${data_int[@]:$server_index:4}")"
          dns_servers=("${dns_servers[@]}" "${dns_server}")
          server_index="$((server_index + 4))"
        done
        echo "  Domain name servers: $(join , "${dns_servers[@]}")"
        ;;
      15)
        echo "  Domain name: $(hex_to_string "${data_hex[@]}")"
        ;;
      51)
        local lease_time="$(printf "%d"  0x$(join '' "${data_hex[@]}"))"
        local lease_time_comment="$(seconds_compare "$lease_time" "$lease_age")"
        echo "  Lease Time: ${lease_time} seconds (${lease_time_comment})"
        ;;
      53)
        echo "  Message type: ${message_types[$data_int]-UNKNOWN}"
        ;;
      58)
        local renew_time="$(printf "%d"  0x$(join '' "${data_hex[@]}"))"
        local renew_time_comment="$(seconds_compare "$renew_time" "$lease_age")"
        echo "  Renew Time: $renew_time seconds (${renew_time_comment})"
        ;;
      59)
        local rebind_time="$(printf "%d"  0x$(join '' "${data_hex[@]}"))"
        local rebind_time_comment="$(seconds_compare \
                                     "$rebind_time" "$lease_age")"
        echo "  Rebinding Time: $rebind_time seconds (${rebind_time_comment})"
        ;;
    esac
  done
}

# Anonymize MAC addresses so they are not transmitted
mac_anonymize () {
  if [ "${anonymize_macs}" != "yes" ]; then
    cat
    return
  fi
  sed -e \
    's/\([0-9A-Fa-f][0-9A-Fa-f]\):\([0-9A-Fa-f][0-9A-Fa-f]\):\([0-9A-Fa-f][0-9A-Fa-f]\):[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]:[0-9A-Fa-f][0-9A-Fa-f]/\1:\2:\3:##:##:##/' \
      -e 's/\([^0-9a-fA-F][0-9a-fA-F]\{8,11\}\)[0-9a-fA-F]\{4\}$/\1####/' \
      -e 's/\([^0-9a-fA-F][0-9a-fA-F]\{8\}\)[0-9a-fA-F]\{4\}\([^0-9a-fA-F]\)/\1####\2/' \
      -e 's/\(\/[0-9a-fA-F]\{8\}\)[0-9a-fA-F]\{4\}\([^0-9a-fA-F]\)/\1####\2/' \
      -e 's/\(\/.*Passphrase \).*/\1[removed]/' \
      -e 's/\(\/.*PSK \).*/\1[removed]/' \
      -e 's/\(\/.*Password \).*/\1[removed]/' \
      -e 's/\(UUID: \).*/\1[removed]/'
}

# Read the logs
tail_logs () {
  files="$(for f in /var/log/messages{.2,.1,} ; do
    [ -f "${f}" ] && echo "${f}";
    done)"
  if [ -n "$*" ] ; then
    tail "$@" ${files} | mac_anonymize
  else
    cat ${files} | mac_anonymize
  fi
}

# List all ethernet devices and their unique manufacturer strings
get_device_list () {
  fn_enter_once "${FUNCNAME}" || return
  echo "Device list:"
  for ifc in $(get_iflist); do
    dir="$(readlink -f "/sys/class/net/${ifc}/device")"
    driver="$(basename "$(readlink "${dir}/driver")")"
    if expr "${dir}" : '.*usb' > /dev/null; then
      type=usb
      vendor="$(cat < "${dir}/../idVendor")"
      device="$(cat "${dir}/../idProduct")"
    elif expr "${dir}" : '.*pci' > /dev/null; then
      type=pci
      vendor="$(sed -e 's/0x//' < "${dir}/vendor")"
      device="$(sed -e 's/0x//' < "${dir}/device")"
    else
      type=unknown
    fi
    echo -e "${ifc}\t${type}:${device}:${vendor}\t${driver}"
  done
}

# List all USB and PCI devices
diag_devs () {
  fn_enter_once "${FUNCNAME}" || return
  ${LSUSB}
  ${LSPCI}
}

# Check whether we are in sync with the time server
diag_date () {
  local host="$1"
  fn_enter "${FUNCNAME}" "$@"
  echo "Local time of day: $(date)"
  date_out="$("${TLSDATE}" -nv -H "${host}" 2>&1 >/dev/null)"
  date_exit="$?"
  offset="$(echo "$date_out" | grep 'difference is' | cut -f8 -d' ')"
  if [ "${date_exit}" != 0 ]; then
    fail "Unable to get date via tlsdate from ${host}: ${date_out}"
  elif [ "${offset}" = 0 ] ; then
    pass "Time appears to be correct"
  elif [ "${offset}" -lt -3600 -o "${offset}" -gt 3600 ] ; then
    fail "Time offset = ${offset}"
  else
    pass "Time offset is small (${offset})"
  fi
}

# Try to detect IP address collisions
diag_ip_collision () {
  fn_enter "${FUNCNAME}" "$@"
  local ifc="$1"
  ip="$(get_if_addr "${ifc}")"
  if [ -z "${ip}" ]; then
    fail "${ifc} does not have IP address"
    return 1
  fi
  if ! "${ARPING}" -c 3 -I "${ifc}" -D "${ip}"; then
    fail "IP Address Collision Detected!"
    return 1
  fi
  return 0
}

# Make sure we have an ARP table entry for $ip through $ifc
diag_arp () {
  fn_enter "${FUNCNAME}" "$@"
  local ip="$1"
  local ifc="$2"
  local failures=0

  if [ -n "${ip}" ] ; then
    arp="$(${ARP} -an | awk '/('${ip}').*'${ifc}'$/ { print $4 }')"
    if [ -z "${arp}" ]; then
      fail "Arp table does not contain entry for ${ip}"
      failures="$((failures + 1))"
    elif [ "${arp}" = "<incomplete>" ] ; then
      fail "Can't arp for ${ip}"
      diag_ip_collision "${ifc}"
      failures="$((failures + 1))"
    else
      pass "ARP for ${ip} is ${arp}" | mac_anonymize
    fi
  fi

  fn_enter_once "${FUNCNAME}_table" && "${ARP}" -an | mac_anonymize

  return "${failures}"
}

show_routes () {
  netstat -nr
  echo "Raw routing tables:"
  cat /proc/net/route
  cat /proc/net/ipv6_route
}

# Make sure we have a route to each host
diag_route () {
  fn_enter "${FUNCNAME}" "$@"
  local failures=0
  for ip in "$@"; do
    ifinfo="$(get_if_route "${ip}")"
    gwinfo="$(get_gw_route "${ip}")"
    if [ -n "${ifinfo}" ]; then
      diag_arp "${ip}" "${ifinfo}" || failures="$((failures + 1))"
    elif [ -n "${gwinfo}" ]; then
      diag_arp "${gwinfo}" || failures="$((failures + 1))"
    else
      fail "No route to host ${ip}"
      diag_ifall || failures="$((failures + 1))"
    fi
  done

  fn_enter_once "${FUNCNAME}_table" && show_routes

  return "${failures}"
}

# Figure whether we have IP connectivity to each host
diag_ping () {
  fn_enter "${FUNCNAME}" "$@"
  local failures=0
  for ip in "$@"; do
    if ping -c 3 "${ip}" | grep -q '0 received'; then
      fail "Ping to ${ip} failed"
      if diag_route "${ip}"; then
        fail "We were able to reach the router but we cannot get packets to"
        fail "any machines on the other side.  The problem is probably with"
        fail "the router configuration or connectivity and not this system."
      else
        fail "We were successfully able to join the network, but we cannot"
        fail "seem to reach the router right now.  This is either a link"
        fail "level issue, the router is down, or our lease is expired."
        diag_linkall
        diag_dhcp_lease
      fi
      failures="$((failures + 1))"
    else
      pass "address ${ip}: ping OK"
    fi
  done

  return "${failures}"
}

# Report the last DHCP interaction for a given network interface
diag_dhcp () {
  fn_enter_once "${FUNCNAME}" || return
  local ifc="$1"
  shift

  local -a dhcp_event=($(tail_logs "$@" | \
    grep "dhcpcd.*event.*on interface ${ifc}" | \
    sed -e 's/^\([^ ]*\).*event \([A-Z]*\).*/\1 \2/' | tail -1))

  last_dhcpcd_event_time="${dhcp_event[0]}"
  last_dhcpcd_event_type="${dhcp_event[1]}"
  case ${last_dhcpcd_event_type} in
    RENEW|BOUND|REBOOT)
      pass "${ifc}: last dhcp event was successful:" \
        "${last_dhcpcd_event_type} at ${last_dhcpcd_event_time}"
      ;;
    *)
      fail "${ifc}: last dhcp event was" \
        "${last_dhcpd_event_type} at ${last_dhcpcd_event_time}"
      fail "This could be a link-level connectivity problem"
      ;;
  esac
}

# Perform diagnostics on an 802.11 interface
diag_link_wifi () {
  fn_enter "${FUNCNAME}" "$@"
  ifc="$1"

  echo "wifi stats (all interfaces):"
  diag_wifi

  last_connect="$(grep -n "${ifc}: connect SSID" /var/log/messages{.1,})"
  if [ -z "${last_connect}" ] ; then
    fail "${ifc}: no recent 802.11 connection attempts"
    return 1
  fi

  local -a lastconn_info=($(IFS=" :"; echo "${last_connect}"))
  file="${lastconn_info[0]}"
  line="${lastconn_info[1]}"
  size="$(wc -l "${file}" | awk '{print $1}')"
  tail="$((size - line + 1))"
  state_changes="$(tail -"${tail}" "${file}" | grep 'state change')"
  last_state_change="$(grep 'state change' "${file}" | tail -1 | \
                      sed -e 's/.*state change //')"
  echo "${ifc}: last flimflam state change: ${last_state_change}"
  if echo "${state_changes}" | grep -q ' -> COMPLETED'; then
    pass "${ifc}: last connection attempt appears successful"
    return 0
  elif echo "${state_changes}" | \
    grep -q '4WAY_HANDSHAKE -> DISCONNECTED'; then
    fail "${ifc}: It appears that the wrong WPA PSK was used"
  fi

  echo "wpa_supplicant network blocks:"
  wpa_cli list_networks

  diag_dhcp "${ifc}" "-${tail}" "${file}"
}

# Print out WiFi debugging info
diag_wifi () {
  fn_enter_once "${FUNCNAME}" || return

  # Call out to debugd (which in turn calls this script's diag_wifi_internal
  # function with the debugd privileges needed to read debugfs contents).
  dbus-send --system --print-reply --fixed --dest=org.chromium.debugd \
        /org/chromium/debugd org.chromium.debugd.GetLog "string:wifi_status"
}

diag_wifi_internal () {
  # Only callable by debugd or root, since debugfs is locked down.
  for dir in /sys/kernel/debug/ieee80211/phy*/ath9k; do
    [ -d "${dir}" ] && head -1000 "${dir}"/{dma,interrupt,recv,xmit,samples} | \
      mac_anonymize
  done
  for ifc in $(get_iflist); do
    if is_wifi "${ifc}" ; then
      echo "iw dev ${ifc} survey dump:"
      "${IW}" dev "${ifc}" survey dump | mac_anonymize
      echo "iw dev ${ifc} station dump:"
      "${IW}" dev "${ifc}" station dump | mac_anonymize
      echo "iw dev ${ifc} scan dump:"
      "${IW}" dev "${ifc}" scan dump | mac_anonymize | grep -v SSID
      echo "iw dev ${ifc} link:"
      "${IW}" dev "${ifc}" link | mac_anonymize | grep -v SSID
    fi
  done
}

# Perform diagnostics on a WAN interface
diag_link_cellular () {
  fn_enter "${FUNCNAME}" "$@"
  diag_cellular_dbus
  diag_devs
  qcdev="$(get_class_driver tty/ttyUSB qcserial)"
  if [ -n "${qcdev}" ] ; then
    echo "QCSerial device is ${qcdev}"
  fi
  tail_logs | grep "QDL unable" | tail -3
}

diag_link_wired () {
  fn_enter "${FUNCNAME}" "$@"
  local ifc="$1"

  echo "Output from ethtool ${ifc}:"
  "${ETHTOOL}" "${ifc}"

  return 0
}

# Tests to check to see if this is a modem.  Very abstract adaptation
# from flimflam's tests
is_modem () {
  local ifc="$1"
  driver="$(basename "$(readlink "/sys/class/net/${ifc}/device/driver")")"
  # Whitelist certain device types
  [ "${driver}" = "QCUSBNet2k" ] && return 0

  # See if there is a TTY device that is associated the same USB device
  dev_root="$(readlink -f /sys/class/net/${ifc}/device)"
  if expr "${dev_root}" : '.*usb' > /dev/null; then
    local -a tty_devs=($(echo $(dirname ${dev_root})/*/*/tty))
    [ -e "${tty_devs[0]}" ] && return 0
  fi

  return 1
}

is_wifi () {
  local ifc="$1"
  if expr ${ifc} : wlan > /dev/null || \
     [ -e "/sys/class/net/${ifc}/phy80211" ] ; then
    return 0
  fi
  return 1
}

# Perform type-specific link diagnostics on a network interface
diag_link () {
  fn_enter "${FUNCNAME}" "$@"
  ifc="$1"

  if [ "$(cat "/sys/class/net/${ifc}/carrier")" -eq 1 ]; then
    pass "${ifc}: link detected"
  else
    pass "${ifc}: link not detected"
  fi

  if is_wifi "${ifc}" ; then
    diag_link_wifi "${ifc}"
  elif is_modem "${ifc}"; then
    diag_link_cellular "${ifc}"
  else
    diag_link_wired "${ifc}"
  fi

  echo "Last 10 kernel messages for ${ifc}:"
  tail_logs | grep "kernel:.*${ifc}" | tail -10
}

diag_linkall () {
  fn_enter_once "${FUNCNAME}" || return
  for ifc in $(get_iflist); do
    diag_link "${ifc}"
  done
}

# Perform generic diagnostics on a network interface
diag_if () {
  fn_enter "${FUNCNAME}" "$@"
  ifc="$1"
  config="$("${IFCONFIG}" "${ifc}")"
  ret=0
  ${IFCONFIG} "${ifc}" | mac_anonymize
  if ! echo "${config}" | grep -q ' UP '; then
    fail "${ifc} is not listed as up"
    ret=1
  elif ! echo "${config}" | grep -q ' RUNNING '; then
    fail "${ifc} is not listed as running"
    ret=1
  fi

  addr="$(get_if_addr "${ifc}")"
  if [ -n "${addr}" ]; then
    pass "${ifc} assigned IP address ${addr}"
    diag_dhcp "${ifc}"
  else
    fail "${ifc} is not assigned an IP address"
    ret=1
  fi
  diag_link "${ifc}"
  return "${ret}"
}

# Query interface status on all network interfaces, and return an error if
# none of them appear to be up
diag_ifall () {
  fn_enter_once "${FUNCNAME}" || return

  local good_ifs=0
  for ifc in $(get_iflist); do
    diag_if "${ifc}" && good_ifs="$((good_ifs + 1))"
  done
  if [ "${good_ifs}" -eq 0 ] ; then
    fail "No good interfaces found.  You are not connected to a network."
    get_status shill
    get_status wpa_supplicant
    get_status cromo
    return 1
  fi
  diag_flimflam
  return 0
}

# Diagnose nameserver connectivity
diag_nameservers () {
  fn_enter_once "${FUNCNAME}" || return
  nameservers="$(get_nameservers)"
  if [ -z "${nameservers}" ] ; then
    fail "No nameservers -- this is either a network failure or net misconfig"
    get_tracelink /etc/resolv.conf
    diag_ifall
  else
    echo "Testing connectivity to nameservers"
    if diag_ping "$(get_nameservers)"; then
      fail "We can reach the nameservers but were not able to resolve hostnames"
      fail "You may be behind a captive portal or there may be a DNS"
      fail "configuration problem"
    fi
  fi
}

# See if we can connect to a given host
diag_connectivity () {
  fn_enter_once "${FUNCNAME}" "$@" || exit 0
  local host="$1"
  local ip="$(get_host_addr "$host")"
  if [ -z "${ip}" ] ; then
    fail "Could not lookup host ${host}"
    diag_route
    diag_nameservers
    return 1
  fi

  if diag_ping "${ip}"; then
    fail "We were able to ping ${host} but were not able to connect to it."
    fail "This probably means that you are behind a portal or (unlikely)"
    fail "${host} is encountering technical difficulties"
  fi
}

# Get information from flimflam about its state over D-Bus.
dbus_get_object_properties () {
  local entity="$1"
  local object_type="$2"
  local object_path="${3-/}"
  dbus-send --fixed --system --dest="${entity}" --print-reply \
    "${object_path}" "${entity}.${object_type}.GetProperties"
}

dbus_get_object_list () {
  local entity="$1"
  local parent="$2"
  local child="$3"
  local parent_path="${4-/}"
  # The child (property) we are looking for is something like "Devices"
  # or "LinkMonitorResponseTime".  The sed manipulation below will return
  # the list of values for each entry.  For example, with an output that
  # looks like:
  #   /8/Devices/0 /device/wlan0
  #   /8/Devices/1 /device/eth1
  #   /8/Devices/2 /device/eth0
  # and a search for "Devices", we'd return a newline separated
  # "/device/wlan0 /device/eth1 /device/eth0"
  dbus_get_object_properties "${entity}" "${parent}" "${parent_path}" | \
    awk '/^\/[0-9]+\/'${child}'[\/ ]/ { print $2 }'
}

diag_flimflam_dbus () {
  fn_enter_once "${FUNCNAME}" "$@"
  local ff="org.chromium.flimflam"
  if [ -z "$1" ] ; then
    echo "Flimflam Manager:"
    dbus_get_object_properties "${ff}" Manager / | mac_anonymize
    # For each Service defined on the Manager, list its properties
    diag_flimflam_dbus Manager Service
    # For each Device defined in the Manager, list its properties
    diag_flimflam_dbus Manager Device
  else
    local parent="$1"
    local child="$2"
    local parent_path="${3-/}"
    for path in \
      $(dbus_get_object_list "${ff}" "${parent}" "${child}s" \
        "${parent_path}"); do
      echo "${child} ${path}" | mac_anonymize
      dbus_get_object_properties "${ff}" "${child}" "${path}" | mac_anonymize
      if [ "${child}" = Device ] ; then
        # For each Network defined in each Device, list its properties
        diag_flimflam_dbus Device Network "${path}"
      fi
    done
  fi
}

# Get information from ModemManager about its state over D-Bus
diag_cellular_dbus () {
  fn_enter "${FUNCNAME}" "$@"
  dbus-send --fixed --system --print-reply --dest=org.chromium.ModemManager \
    /org/chromium/ModemManager \
    org.freedesktop.ModemManager.EnumerateDevices || \
    fail "Could not contact ModemManager!"
}

# Read lease information
diag_dhcp_lease () {
  fn_enter "${FUNCNAME}" "$@"
  local lease_dir=/var/lib/dhcpcd
  for file in ${lease_dir}/*.lease; do
    echo "DHCP Lease file ${file}"
    read_dhcp_lease "${file}" | mac_anonymize
  done
}

# Probe process status of flimflam and its process descendants
diag_flimflam () {
  fn_enter_once "${FUNCNAME}" || return
  local status="$(get_status shill)"
  echo "${status}"
  if ! echo "${status}" | fgrep -q 'running'; then
    return
  fi
  local pid="$(echo ${status} | awk '{print $4}')"
  ps jx | awk '/^ *'${pid}'/ {print $10,$11,$12}' | sort | uniq -c
  echo "Listing of /var/run/shill"
  ls -al /var/run/shill
  diag_flimflam_dbus
}

run_webget () {
  local url="$1"
  local timeout_secs=10
  echo "Trying to contact ${url} ... (waiting up to ${timeout_secs} seconds)"
  curl -s "${url}" --max-time "${timeout_secs}" > /dev/null
}

# Try to connect to a host via SSL, diagnose failures
diag_webget () {
  local host="$1"
  local url="https://${host}"
  run_webget "${url}"
  local err="$?"

  case ${err} in
    0)
      pass "We can get to ${url} just fine"
      ;;

    6)
      # DNS resolution error
      fail "Got DNS resolution error -- trying to debug nameservers"
      diag_nameservers
      ;;

    7)
      # Failed to connect to host
      fail "Got connection error -- trying to debug connection to host"
      diag_connectivity "${host}"
      ;;

    28)
      # Operation timed out
      fail "Operation timed out during connection to ${host}"
      diag_connectivity "${host}"
      diag_ifall
      ;;

    35)
      # SSL connect error. The SSL handshaking failed
      fail "SSL connect error. The SSL handshaking failed"
      diag_connectivity "${host}"
      diag_ifall
      ;;

    60)
      # Peer certificate cannot be authenticated with known CA certificates
      fail "Peer cert not authenticated -- probably a captive portal!"
      ;;

    *)
      fail "Encountered unhandled curl error ${err}"
      ;;
  esac
  if [ "${err}" -ne 0 ] ; then
      get_device_list
      diag_flimflam
  fi
  return "$err"
}

# Do standard run of tests
diag_run () {
  local host="$1"
  if run_webget "https://${host}"; then
    pass "Loaded $host via HTTPS"
  elif run_webget "http://${host}"; then
    pass "Loaded $host via HTTP (ignore any tlsdate failure below)"
  else
    diag_webget "${host}"
  fi
  diag_date "${host}"
  diag_gateway_latency "${host}"
}


monitor_get_stats () {
  local tmp_file="$(mktemp /tmp/iwlink.XXXXX)"
  local err_file="$(mktemp /tmp/iwlink.XXXXX)"
  "${IW}" dev "${mon_wlan_device}" link > "${tmp_file}"
  if ! grep -q 'Connected to' "${tmp_file}"; then
    values_reset=0
    unlink "${tmp_file}"
    unlink "${err_file}"
    return
  fi
  for param in ${mon_link_params[@]}; do
    val="$(grep "${param}" "${tmp_file}" | \
          sed -e "s/.*${param}:*[ 	]*\([^ 	]*\).*/\1/")"
    mon_current["${param}"]="${val}"
  done
  "${IW}" dev "${mon_wlan_device}" station dump 2>"${err_file}" > "${tmp_file}"
  if [ "$?" -ne 0 -o -s "${err_file}" ] ; then
    values_reset=0
    unlink "${tmp_file}"
    unlink "${err_file}"
    return
  fi
  for param in ${mon_station_params[@]}; do
    val="$(grep "${param}" "${tmp_file}" | \
          sed -e "s/.*${param}:*[ 	]*\([^ 	]*\).*/\1/")"
    mon_current["${param}"]="${val}"
  done
  "${IW}" dev "${mon_wlan_device}" survey dump 2>"${err_file}" | \
    grep -A6 "${mon_current[freq]}" > "${tmp_file}"
  if [ "$?" -ne 0 -o -s "${err_file}" ] ; then
    values_reset=0
    unlink "${tmp_file}"
    unlink "${err_file}"
    return
  fi
  for param in ${mon_survey_params[@]}; do
    val="$(grep "${param}" "${tmp_file}" | \
          sed -e "s/.*${param}:*[ 	]*\([^ 	]*\).*/\1/")"
    mon_current["${param}"]="${val}"
  done
  echo -n "$(date +%s)"
  for param in ${mon_delta_params[@]}; do
    if [ -n "${values_reset}" ] ; then
      prev=0
    else
      prev="${mon_last["${param}"]-0}"
    fi
    val="$((mon_current[${param}] - $prev))"
    mon_last["${param}"]="${mon_current["${param}"]}"
    echo -n " ${param}:${val}"
  done
  for param in ${mon_static_params[@]}; do
    echo -n " ${param}:${mon_current[${param}]}"
  done
  echo
  values_reset=
  unlink "${tmp_file}"
  unlink "${err_file}"
}

monitor_cleanup () {
  kill "${mon_event_proc}"
  [ -n "${mon_msgs_proc}" ] && kill "${mon_msgs_proc}"
  echo -n TOTALS:
  for param in ${mon_delta_params[@]} ${mon_static_params[@]}; do
    echo -n " ${param}:${mon_current[${param}]}"
  done
  exit 0
}

diag_wifi_monitor () {
  if ! [ -d /sys/bus/pci/drivers/ath9k ] ; then
    echo "Unfortunately this script only spports ath9k at the moment."
    return
  fi
  declare -A mon_last
  declare -A mon_current
  mon_wlan_device=wlan0

  declare -a mon_link_params
  mon_link_params=( freq signal 'tx.bitrate' 'dtim.period' 'beacon.int' SSID \
                    Connected.to )
  declare -a mon_delta_station_params
  declare -a mon_static_station_params
  declare -a mon_station_params
  mon_delta_station_params=( 'rx.bytes' 'tx.bytes' 'rx.packets' \
                             'tx.packets' 'tx.retries' 'tx.failed' )
  mon_static_station_params=( 'inactive.time' 'signal.avg' )
  mon_station_params=("${mon_static_station_params[@]}" \
                      "${mon_delta_station_params[@]}")
  declare -a mon_delta_survey_params
  declare -a mon_static_survey_params
  declare -a mon_survey_params
  mon_delta_survey_params=( 'channel.active.time' 'channel.busy.time' \
                            'channel.receive.time' 'channel.transmit.time' )
  mon_static_survey_params=( noise )
  mon_survey_params=("${mon_static_survey_params[@]}" \
                     "${mon_delta_survey_params[@]}")
  mon_static_params=("${mon_static_survey_params[@]}" "${mon_link_params[@]}" \
                     "${mon_static_station_params[@]}")
  mon_delta_params=("${mon_delta_survey_params[@]}" \
                    "${mon_delta_station_params[@]}")
  values_reset=0

  trap monitor_cleanup SIGINT SIGTERM
  "${IW}" event -t &
  mon_event_proc="$!"
  mon_debug_file=/sys/kernel/debug/ieee80211/phy0/ath9k/debug
  if [ -f "${mon_debug_file}" -a -r /proc/kmsg ] ; then
    [ -w "${mon_debug_file}" ] && echo 0x440 > "${mon_debug_file}"
    # NB: Even if we can't write to the flag, debug might already enabled
    egrep 'ath:.*(cal|ANI)' /proc/kmsg &
    mon_msgs_proc="$!"
  fi
  while sleep 1; do
    monitor_get_stats
  done
}

diag_gateway_latency () {
  local ff="org.chromium.flimflam"
  local services=($(dbus_get_object_list "${ff}" Manager Services /))
  local service="${services[0]}"
  local latency_threshold=800  # milliseconds
  if [ -z "${service}" ]; then
    fail "Found no services."
    return
  fi
  local device="$(dbus_get_object_list "${ff}" Service Device "${service}")"
  if [ -z "${device}" ]; then
    fail "Service ${service} does not have an associated Device."
    return
  fi
  local monitor_val="$(dbus_get_object_list "${ff}" Device \
                      LinkMonitorResponseTime "${device}")"
  if [ -z "${monitor_val}" ]; then
    echo "Service ${service} on device ${device} has no current latency value"
    return
  fi
  message="Current LinkMonitor latency for ${device} is ${monitor_val}ms"
  if [ "${monitor_val}" -le "${latency_threshold}" ]; then
    pass "$message"
  else
    fail "$message"
  fi
}

usage () {
  echo "Usage: $0 [--date|--flimflam|--link|--show-macs|--wifi|--help|"
  echo "           --wifi-mon] [host]"
  echo "    --date:      Diagnose time-of-day"
  echo "                 ([host] must support SSL)"
  echo "    --dhcp:      Display DHCP information"
  echo "    --flimflam:  Diagnose flimflam status"
  echo "    --interface: Diagnose interface status"
  echo "    --latency:   Diagnose link-latency to default gateway"
  echo "    --link:      Diagnose all network links"
  echo "    --no-log:    Do not log output"
  echo "    --route:     Diagnose routes to each host"
  echo "    --show-macs: Display full MAC addresses"
  echo "    --wifi:      Display driver-specific debugging information"
  echo "    --wifi-mon:  Monitor WiFi performance metrics"
  echo "    --help:      Display this message"
  echo "    [host]       Hostname to perform web access test on " \
                         "(default: www.google.com)"
}

check_no_arg () {
  if [[ "$2" == "yes" ]]; then
    echo "Option $1 does not take any arguments"
    usage
    exit 1
  fi
}

main () {
  local test_host=www.google.com
  local option_and_arg_provided=no
  local option
  local param
  local rest=()
  for param in "$@"; do
    # Arguments that modify the behavior of diagnostic modules
    # should be placed here. This ensures that they are processed
    # before running the diagnostics.
    case ${param} in
      --show-macs)
        anonymize_macs=no
        ;;
      --no-log)
        # Handled below before starting main
        ;;
      *)
        rest+=( "${param}" )
        ;;
    esac
  done
  case ${#rest[@]} in
    0)
      diag_run ${test_host}
      return
      ;;
    1)
      if [[ ${rest[0]} == --* ]]; then
        option=${rest[0]}
      else
        test_host=${rest[0]}
        diag_run ${test_host}
        return
      fi
      ;;
    2)
      option=${rest[0]}
      test_host=${rest[1]}
      option_and_arg_provided=yes
      ;;
    *)
      echo "Too many arguments"
      usage
      exit 1
      ;;
  esac
  if [[ -n "${option}" ]]; then
    case ${option} in
      --date)
        diag_date "${test_host}"
        ;;
      --dhcp)
        check_no_arg "${option}" "${option_and_arg_provided}"
        diag_dhcp_lease
        ;;
      --flimflam)
        check_no_arg "${option}" "${option_and_arg_provided}"
        diag_flimflam
        ;;
      --interface)
        check_no_arg "${option}" "${option_and_arg_provided}"
        diag_ifall
        ;;
      --latency)
        check_no_arg "${option}" "${option_and_arg_provided}"
        diag_gateway_latency
        ;;
      --link)
        check_no_arg "${option}" "${option_and_arg_provided}"
        diag_linkall
        ;;
      --route)
        diag_route "${test_host}"
        ;;
      --wifi)
        check_no_arg "${option}" "${option_and_arg_provided}"
        diag_wifi
        ;;
      --wifi-internal)
        # Only called by debugd.
        check_no_arg "${option}" "${option_and_arg_provided}"
        diag_wifi_internal
        exit 0
        ;;
      --wifi-mon)
        check_no_arg "${option}" "${option_and_arg_provided}"
        diag_wifi_monitor
        ;;
      --help|-help|-h)
        check_no_arg "${option}" "${option_and_arg_provided}"
        usage
        exit 0
        ;;
      *)
        echo "Unknown option: $option"
        usage
        exit 1
        ;;
    esac
    exit "${fail_count}"
  fi
}

if [ -d "/home/${USER}/user/Downloads" ] && \
    ! expr "$*" : '.*--no-log' >/dev/null; then
  # Log this output to somewhere the user can upload from
  main "$@" 2>&1 | \
    tee "$(unique_file \
           "/home/${USER}/user/Downloads/network_diagnostics_\*.txt")"
else
  main "$@"
fi


exit "${fail_count}"
